package cw.spring.security.acl.poc.security;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.log4j.Logger;
import org.springframework.security.AccessDecisionManager;
import org.springframework.security.Authentication;
import org.springframework.security.AuthenticationException;
import org.springframework.security.AuthenticationManager;
import org.springframework.security.context.SecurityContextHolder;
import org.springframework.security.providers.AuthenticationProvider;
import org.springframework.security.providers.ProviderManager;
import org.springframework.security.providers.UsernamePasswordAuthenticationToken;
import org.springframework.security.vote.RoleVoter;
import org.springframework.security.vote.UnanimousBased;

/**
 * Does basic security tasks :
 * <ul>
 * <li>Authentication against a org.acegisecurity.userdetails.UserDetailsService</li>
 * <li>Keeping current sessions in a map</li>
 * <li>Storing session in each client threads</li>
 * </ul>
 * 
 * @author agadby
 * @author montagnoncy
 * 
 */
public class AcegiSecurityManager {
  // Must be provided by each applications
  private AccessDecisionManager accessDecisionManager;
  private AuthenticationManager authenticationManager;

  private long sessionDuration = 0;

  private static final Logger log = Logger.getLogger(AcegiSecurityManager.class);

  public AcegiSecurityManager() {
    // default AccessDecisionManager
    UnanimousBased unanimousDecisionManager = new UnanimousBased();
    List<RoleVoter> votersList = new ArrayList<RoleVoter>();
    votersList.add(new RoleVoter());
    unanimousDecisionManager.setDecisionVoters(votersList);
    setAccessDecisionManager(unanimousDecisionManager);
  }

  // Internal fields
  private Map<String, Session> sessions = new HashMap<String, Session>();

  /*
   * (non-Javadoc)
   * 
   * @see north.util.security.NorthSecurityManager#setSessionContext(java.lang.String)
   */
  public void setSessionContext(String sessionId) {
    // We add the authentication to the SecurityContextHolder
    boolean isAuthenticated = sessionId != null;
    if (isAuthenticated) {
      Session northSession = sessions.get(sessionId);
      if (northSession != null) {
        checkSessionValidity(sessionId, northSession);
        northSession.setLastCallTimestamp(System.currentTimeMillis());
        SecurityContextHolder.getContext().setAuthentication(northSession.getAuthentication());
      } else {
        log.debug("Invalid session : " + sessionId);
      }
    }
  }

  /**
   * Checks that the session is still valid.
   * 
   * @param sessionId
   * @param northSession
   */
  private void checkSessionValidity(String sessionId, Session northSession) {
    if (sessionDuration != 0 && (System.currentTimeMillis() - northSession.getLastCallTimestamp()) > sessionDuration) {
      log.debug("The session " + sessionId + " has expired");
      Session removedSession = sessions.remove(sessionId);
      if (removedSession != null) {
        log.error("Unable to remove the session " + sessionId + " from the list of active sessions");
      }
    }
  }

  public void removeSession(String key) {
    sessions.remove(key);
  }

  /*
   * (non-Javadoc)
   * 
   * @see north.util.security.NorthSecurityManager#clearSessionContext()
   */
  public void clearSessionContext() {
    SecurityContextHolder.clearContext();
  }

  /*
   * (non-Javadoc)
   * 
   * @see north.util.security.NorthSecurityManager#authenticate(java.lang.String, java.lang.String)
   */
  public String authenticate(String login, String password) {
    // TODO : better generation of session Id
    log.info("trying to authenticate " + login);
    String sessionId = "" + login.hashCode() + System.currentTimeMillis();
    Authentication auth = new UsernamePasswordAuthenticationToken(login, password);
    try {
      auth = authenticationManager.authenticate(auth);
    } catch (AuthenticationException e) {
    }
    SecurityContextHolder.getContext().setAuthentication(auth);
    sessions.put(sessionId, new Session(auth, System.currentTimeMillis()));
    log.info("new session initialized for " + login);
    return sessionId;
  }

  public boolean checkCredentials(String login, String password) {
    log.info("trying to check credentials of " + login);
    try {
      authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(login, password));
      return true;
    } catch (AuthenticationException e) {
      return false;
    }
  }

  public AccessDecisionManager getAccessDecisionManager() {
    return accessDecisionManager;
  }

  public void setAccessDecisionManager(AccessDecisionManager accessDecisionManager) {
    this.accessDecisionManager = accessDecisionManager;
  }

  public void setSessionDuration(long sessionDuration) {
    this.sessionDuration = sessionDuration;
  }

  public void setAuthenticationManager(AuthenticationManager authenticationManager) {
    this.authenticationManager = authenticationManager;
  }

  public void setAuthenticationProvider(AuthenticationProvider authenticationProvider) {
    if (authenticationProvider == null) {
      throw new RuntimeException("AuthenticationServiceImpl needs at least 'providerManager' or 'userDetailsService' property");
    }
    ProviderManager newProvider = new ProviderManager();
    List<AuthenticationProvider> providerList = new ArrayList<AuthenticationProvider>();
    providerList.add(authenticationProvider);
    newProvider.setProviders(providerList);
    authenticationManager = newProvider;
  }
}
